<html><head><meta charset="utf-8"/><title>crypto</title></head><body><h1>crypto</h1><div class="x-wiki-content x-main-content">
 <p>
  crypto模块的目的是为了提供通用的加密和哈希算法。用纯JavaScript代码实现这些功能不是不可能，但速度会非常慢。Nodejs用C/C++实现这些算法后，通过cypto这个模块暴露为JavaScript接口，这样用起来方便，运行速度也快。
 </p>
 <h3>
  MD5和SHA1
 </h3>
 <p>
  MD5是一种常用的哈希算法，用于给任意数据一个“签名”。这个签名通常用一个十六进制的字符串表示：
 </p>
 <pre><code>const crypto = require('crypto');

const hash = crypto.createHash('md5');

// 可任意多次调用update():
hash.update('Hello, world!');
hash.update('Hello, nodejs!');

console.log(hash.digest('hex')); // 7e1977739c748beac0c0fd14fd26a544
</code></pre>
 <p>
  <code>
   update()
  </code>
  方法默认字符串编码为
  <code>
   UTF-8
  </code>
  ，也可以传入Buffer。
 </p>
 <p>
  如果要计算SHA1，只需要把
  <code>
   'md5'
  </code>
  改成
  <code>
   'sha1'
  </code>
  ，就可以得到SHA1的结果
  <code>
   1f32b9c9932c02227819a4151feed43e131aca40
  </code>
  。
 </p>
 <p>
  还可以使用更安全的
  <code>
   sha256
  </code>
  和
  <code>
   sha512
  </code>
  。
 </p>
 <h3>
  Hmac
 </h3>
 <p>
  Hmac算法也是一种哈希算法，它可以利用MD5或SHA1等哈希算法。不同的是，Hmac还需要一个密钥：
 </p>
 <pre><code>const crypto = require('crypto');

const hmac = crypto.createHmac('sha256', 'secret-key');

hmac.update('Hello, world!');
hmac.update('Hello, nodejs!');

console.log(hmac.digest('hex')); // 80f7e22570...
</code></pre>
 <p>
  只要密钥发生了变化，那么同样的输入数据也会得到不同的签名，因此，可以把Hmac理解为用随机数“增强”的哈希算法。
 </p>
 <h3>
  AES
 </h3>
 <p>
  AES是一种常用的对称加密算法，加解密都用同一个密钥。crypto模块提供了AES支持，但是需要自己封装好函数，便于使用：
 </p>
 <pre><code>const crypto = require('crypto');

function aesEncrypt(data, key) {
    const cipher = crypto.createCipher('aes192', key);
    var crypted = cipher.update(data, 'utf8', 'hex');
    crypted += cipher.final('hex');
    return crypted;
}

function aesDecrypt(encrypted, key) {
    const decipher = crypto.createDecipher('aes192', key);
    var decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');
    return decrypted;
}

var data = 'Hello, this is a secret message!';
var key = 'Password!';
var encrypted = aesEncrypt(data, key);
var decrypted = aesDecrypt(encrypted, key);

console.log('Plain text: ' + data);
console.log('Encrypted text: ' + encrypted);
console.log('Decrypted text: ' + decrypted);
</code></pre>
 <p>
  运行结果如下：
 </p>
 <pre><code>Plain text: Hello, this is a secret message!
Encrypted text: 8a944d97bdabc157a5b7a40cb180e7...
Decrypted text: Hello, this is a secret message!
</code></pre>
 <p>
  可以看出，加密后的字符串通过解密又得到了原始内容。
 </p>
 <p>
  注意到AES有很多不同的算法，如
  <code>
   aes192
  </code>
  ，
  <code>
   aes-128-ecb
  </code>
  ，
  <code>
   aes-256-cbc
  </code>
  等，AES除了密钥外还可以指定IV（Initial Vector），不同的系统只要IV不同，用相同的密钥加密相同的数据得到的加密结果也是不同的。加密结果通常有两种表示方法：hex和base64，这些功能Nodejs全部都支持，但是在应用中要注意，如果加解密双方一方用Nodejs，另一方用Java、PHP等其它语言，需要仔细测试。如果无法正确解密，要确认双方是否遵循同样的AES算法，字符串密钥和IV是否相同，加密后的数据是否统一为hex或base64格式。
 </p>
 <h3>
  Diffie-Hellman
 </h3>
 <p>
  DH算法是一种密钥交换协议，它可以让双方在不泄漏密钥的情况下协商出一个密钥来。DH算法基于数学原理，比如小明和小红想要协商一个密钥，可以这么做：
 </p>
 <ol>
  <li>
   <p>
    小明先选一个素数和一个底数，例如，素数
    <code>
     p=23
    </code>
    ，底数
    <code>
     g=5
    </code>
    （底数可以任选），再选择一个秘密整数
    <code>
     a=6
    </code>
    ，计算
    <code>
     A=g^a mod p=8
    </code>
    ，然后大声告诉小红：
    <code>
     p=23，g=5，A=8
    </code>
    ；
   </p>
  </li>
  <li>
   <p>
    小红收到小明发来的
    <code>
     p
    </code>
    ，
    <code>
     g
    </code>
    ，
    <code>
     A
    </code>
    后，也选一个秘密整数
    <code>
     b=15
    </code>
    ，然后计算
    <code>
     B=g^b mod p=19
    </code>
    ，并大声告诉小明：
    <code>
     B=19
    </code>
    ；
   </p>
  </li>
  <li>
   <p>
    小明自己计算出
    <code>
     s=B^a mod p=2
    </code>
    ，小红也自己计算出
    <code>
     s=A^b mod p=2
    </code>
    ，因此，最终协商的密钥
    <code>
     s
    </code>
    为
    <code>
     2
    </code>
    。
   </p>
  </li>
 </ol>
 <p>
  在这个过程中，密钥
  <code>
   2
  </code>
  并不是小明告诉小红的，也不是小红告诉小明的，而是双方协商计算出来的。第三方只能知道
  <code>
   p=23
  </code>
  ，
  <code>
   g=5
  </code>
  ，
  <code>
   A=8
  </code>
  ，
  <code>
   B=19
  </code>
  ，由于不知道双方选的秘密整数
  <code>
   a=6
  </code>
  和
  <code>
   b=15
  </code>
  ，因此无法计算出密钥
  <code>
   2
  </code>
  。
 </p>
 <p>
  用crypto模块实现DH算法如下：
 </p>
 <pre><code>const crypto = require('crypto');

// xiaoming's keys:
var ming = crypto.createDiffieHellman(512);
var ming_keys = ming.generateKeys();

var prime = ming.getPrime();
var generator = ming.getGenerator();

console.log('Prime: ' + prime.toString('hex'));
console.log('Generator: ' + generator.toString('hex'));

// xiaohong's keys:
var hong = crypto.createDiffieHellman(prime, generator);
var hong_keys = hong.generateKeys();

// exchange and generate secret:
var ming_secret = ming.computeSecret(hong_keys);
var hong_secret = hong.computeSecret(ming_keys);

// print secret:
console.log('Secret of Xiao Ming: ' + ming_secret.toString('hex'));
console.log('Secret of Xiao Hong: ' + hong_secret.toString('hex'));
</code></pre>
 <p>
  运行后，可以得到如下输出：
 </p>
 <pre><code>$ node dh.js 
Prime: a8224c...deead3
Generator: 02
Secret of Xiao Ming: 695308...d519be
Secret of Xiao Hong: 695308...d519be
</code></pre>
 <p>
  注意每次输出都不一样，因为素数的选择是随机的。
 </p>
 <h3>
  RSA
 </h3>
 <p>
  RSA算法是一种非对称加密算法，即由一个私钥和一个公钥构成的密钥对，通过私钥加密，公钥解密，或者通过公钥加密，私钥解密。其中，公钥可以公开，私钥必须保密。
 </p>
 <p>
  RSA算法是1977年由Ron Rivest、Adi Shamir和Leonard Adleman共同提出的，所以以他们三人的姓氏的头字母命名。
 </p>
 <p>
  当小明给小红发送信息时，可以用小明自己的私钥加密，小红用小明的公钥解密，也可以用小红的公钥加密，小红用她自己的私钥解密，这就是非对称加密。相比对称加密，非对称加密只需要每个人各自持有自己的私钥，同时公开自己的公钥，不需要像AES那样由两个人共享同一个密钥。
 </p>
 <p>
  在使用Node进行RSA加密前，我们先要准备好私钥和公钥。
 </p>
 <p>
  首先，在命令行执行以下命令以生成一个RSA密钥对：
 </p>
 <pre><code>openssl genrsa -aes256 -out rsa-key.pem 2048
</code></pre>
 <p>
  根据提示输入密码，这个密码是用来加密RSA密钥的，加密方式指定为AES256，生成的RSA的密钥长度是2048位。执行成功后，我们获得了加密的
  <code>
   rsa-key.pem
  </code>
  文件。
 </p>
 <p>
  第二步，通过上面的
  <code>
   rsa-key.pem
  </code>
  加密文件，我们可以导出原始的私钥，命令如下：
 </p>
 <pre><code>openssl rsa -in rsa-key.pem -outform PEM -out rsa-prv.pem
</code></pre>
 <p>
  输入第一步的密码，我们获得了解密后的私钥。
 </p>
 <p>
  类似的，我们用下面的命令导出原始的公钥：
 </p>
 <pre><code>openssl rsa -in rsa-key.pem -outform PEM -pubout -out rsa-pub.pem
</code></pre>
 <p>
  这样，我们就准备好了原始私钥文件
  <code>
   rsa-prv.pem
  </code>
  和原始公钥文件
  <code>
   rsa-pub.pem
  </code>
  ，编码格式均为PEM。
 </p>
 <p>
  下面，使用
  <code>
   crypto
  </code>
  模块提供的方法，即可实现非对称加解密。
 </p>
 <p>
  首先，我们用私钥加密，公钥解密：
 </p>
 <pre><code>const
    fs = require('fs'),
    crypto = require('crypto');

// 从文件加载key:
function loadKey(file) {
    // key实际上就是PEM编码的字符串:
    return fs.readFileSync(file, 'utf8');
}

let
    prvKey = loadKey('./rsa-prv.pem'),
    pubKey = loadKey('./rsa-pub.pem'),
    message = 'Hello, world!';

// 使用私钥加密:
let enc_by_prv = crypto.privateEncrypt(prvKey, Buffer.from(message, 'utf8'));
console.log('encrypted by private key: ' + enc_by_prv.toString('hex'));


let dec_by_pub = crypto.publicDecrypt(pubKey, enc_by_prv);
console.log('decrypted by public key: ' + dec_by_pub.toString('utf8'));
</code></pre>
 <p>
  执行后，可以得到解密后的消息，与原始消息相同。
 </p>
 <p>
  接下来我们使用公钥加密，私钥解密：
 </p>
 <pre><code>// 使用公钥加密:
let enc_by_pub = crypto.publicEncrypt(pubKey, Buffer.from(message, 'utf8'));
console.log('encrypted by public key: ' + enc_by_pub.toString('hex'));

// 使用私钥解密:
let dec_by_prv = crypto.privateDecrypt(prvKey, enc_by_pub);
console.log('decrypted by private key: ' + dec_by_prv.toString('utf8'));
</code></pre>
 <p>
  执行得到的解密后的消息仍与原始消息相同。
 </p>
 <p>
  如果我们把
  <code>
   message
  </code>
  字符串的长度增加到很长，例如1M，这时，执行RSA加密会得到一个类似这样的错误：
  <code>
   data too large for key size
  </code>
  ，这是因为RSA加密的原始信息必须小于Key的长度。那如何用RSA加密一个很长的消息呢？实际上，RSA并不适合加密大数据，而是先生成一个随机的AES密码，用AES加密原始信息，然后用RSA加密AES口令，这样，实际使用RSA时，给对方传的密文分两部分，一部分是AES加密的密文，另一部分是RSA加密的AES口令。对方用RSA先解密出AES口令，再用AES解密密文，即可获得明文。
 </p>
 <h3>
  证书
 </h3>
 <p>
  crypto模块也可以处理数字证书。数字证书通常用在SSL连接，也就是Web的https连接。一般情况下，https连接只需要处理服务器端的单向认证，如无特殊需求（例如自己作为Root给客户发认证证书），建议用反向代理服务器如Nginx等Web服务器去处理证书。
 </p>
 <h3>
  参考源码
 </h3>
 <p>
  <a href="https://github.com/michaelliao/learn-javascript/tree/master/samples/node/crypto">
   crypto常用算法
  </a>
 </p>
</div>
</body></html>